package com.asmtest.visitor;

import com.util.description.MethodSignature;
import com.util.description.TypeDesc;
import com.util.description.TypeVariableTypeDesc;
import org.junit.Assert;
import org.junit.Test;
import org.objectweb.asm.signature.SignatureReader;

import java.util.List;

/**
 * @author yutianhong
 * @version 1.0
 * @since 2024/01/01 16:33
 */
public class CustomMethodSignatureVisitorTest {

    @Test
    public void testTypeVariable() {
        // E xxx(E arg0)
        String signature = "<E:Ljava/lang/Object;>(TE;)TE;";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        TypeDesc out = methodSignature.getReturnType();
        Assert.assertEquals("E", in.className());
        Assert.assertTrue(in.isTypeVariable());
        Assert.assertEquals("E", out.className());
        Assert.assertTrue(out.isTypeVariable());
    }

    @Test
    public void testMultiParam() {
        // int xxx(Object arg0, Object... arg1)
        String signature = "(Ljava/lang/Object;[Ljava/lang/Object;)I";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(2, methodSignature.getParamTypes().size());
        TypeDesc in0 = methodSignature.getParamTypes().get(0);
        TypeDesc in1 = methodSignature.getParamTypes().get(1);
        TypeDesc out = methodSignature.getReturnType();
        Assert.assertEquals("java/lang/Object", in0.className());
        Assert.assertEquals("[Ljava/lang/Object", in1.className());
        Assert.assertEquals("java/lang/Object", in1.asArray().getComponentType().className());
        Assert.assertTrue(out.isPrimitive());
        Assert.assertEquals("I", out.className());
    }

    @Test
    public void testGenericWithArray() {
        // List<int[]> xxx(List<Integer[]> arg0)
        String signature = "(Ljava/util/List<[Ljava/lang/Integer;>;)Ljava/util/List<[I>;";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        TypeDesc out = methodSignature.getReturnType();
        TypeDesc listElement = in.asParameterized().getTypeArguments().get(0);
        Assert.assertEquals("[Ljava/lang/Integer", listElement.className());
        Assert.assertEquals("java/lang/Integer", listElement.asArray().getComponentType().className());
        Assert.assertEquals("[I", out.asParameterized().getTypeArguments().get(0).className());
    }

    @Test
    public void testNestedTypeVariable() {
        // <T> void xxx(List<Set<T> arg0)
        String signature = "<T:Ljava/lang/Object;>(Ljava/util/List<Ljava/util/Set<TT;>;>;)V";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        TypeDesc out = methodSignature.getReturnType();

        Assert.assertEquals("java/util/List", in.className());
        Assert.assertEquals("java/util/Set", in.asParameterized().getTypeArguments().get(0).className());
        Assert.assertEquals("T", in.asParameterized().getTypeArguments().get(0)
                .asParameterized().getTypeArguments().get(0).asTypeVariable().className());
        Assert.assertTrue(out.isPrimitive());
        Assert.assertEquals("V", out.className());
    }

    @Test
    public void testMultipleTypeVariable() {
        // <E, K, V> E xxx(Map<K, V> arg0)
        String signature = "<E:Ljava/lang/Object;K:Ljava/lang/Object;V:Ljava/lang/Object;>(Ljava/util/Map<TK;TV;>;)TE;";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        TypeDesc out = methodSignature.getReturnType();
        Assert.assertEquals("K", in.asParameterized().getTypeArguments().get(0).className());
        Assert.assertEquals("E", out.className());
        // type variable
        List<TypeDesc> typeVariables = methodSignature.getTypeVariables();
        Assert.assertSame(3, typeVariables.size());
        Assert.assertEquals("E", typeVariables.get(0).asTypeVariable().className());
        Assert.assertEquals("V", typeVariables.get(2).asTypeVariable().className());
    }

    @Test
    public void testArrayWithGenerics() {
        // List<String[]> xxx(List<String> arg0)
        String signature = "(Ljava/util/List<Ljava/lang/String;>;)Ljava/util/List<[Ljava/lang/String;>;";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        TypeDesc out = methodSignature.getReturnType();
        Assert.assertEquals("java/lang/String", in.asParameterized().getTypeArguments().get(0).className());
        Assert.assertEquals("[Ljava/lang/String", out.asParameterized().getTypeArguments().get(0).className());
    }

    @Test
    public void testNestedWildcardGenerics() {
        // void xxx(List<? extends List<? super Number>> arg0)
        String signature = "(Ljava/util/List<+Ljava/util/List<-Ljava/lang/Number;>;>;)V";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        TypeDesc out = methodSignature.getReturnType();
        TypeDesc firstArg = in.asParameterized().getTypeArguments().get(0);
        Assert.assertTrue(firstArg.isWildcard());
        TypeDesc nestFirstArg = firstArg.asWildcard().getUpperBounds().get(0)
                .asParameterized().getTypeArguments().get(0);
        Assert.assertTrue(nestFirstArg.isWildcard());
        Assert.assertEquals("java/lang/Number", nestFirstArg.asWildcard().getLowerBounds().get(0).className());
        Assert.assertEquals("V", out.className());
    }

    @Test
    public void testWildcardGenerics() {
        // void xxx(Map<? extends List<?>, ?> arg0)
        String signature = "(Ljava/util/Map<+Ljava/util/List<*>;*>;)V";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        TypeDesc out = methodSignature.getReturnType();
        TypeDesc firstArg = in.asParameterized().getTypeArguments().get(0);
        TypeDesc secondArg = in.asParameterized().getTypeArguments().get(1);
        Assert.assertTrue(firstArg.isWildcard());
        TypeDesc nestFirstArg = firstArg.asWildcard().getUpperBounds().get(0)
                .asParameterized().getTypeArguments().get(0);
        Assert.assertTrue(nestFirstArg.isWildcard());
        Assert.assertEquals("?", nestFirstArg.asWildcard().className());
        Assert.assertEquals("?", secondArg.asWildcard().className());
        Assert.assertEquals("V", out.className());
    }

    @Test
    public void testComplexNestedGenerics() {
        // void xxx(Map<String, List<Set<Integer[]>>> arg0)
        String signature = "(Ljava/util/Map<Ljava/lang/String;Ljava/util/List<Ljava/util/Set<[Ljava/lang/Integer;>;>;>;)V";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        Assert.assertEquals("java/util/Map", in.className());
        Assert.assertEquals("java/lang/String", in.asParameterized().getTypeArguments().get(0).className());
        TypeDesc secondArg = in.asParameterized().getTypeArguments().get(1);
        Assert.assertEquals("java/util/List", secondArg.className());
        TypeDesc nestArg = secondArg.asParameterized().getTypeArguments().get(0).asParameterized().getTypeArguments().get(0);
        Assert.assertEquals("[Ljava/lang/Integer", nestArg.className());
        Assert.assertEquals("java/lang/Integer", nestArg.asArray().getComponentType().className());
    }

    @Test
    public void testGenericsWithBounds() {
        // <T extends List<String>> void xxx(T arg0)
        String signature = "<T:Ljava/util/List<Ljava/lang/String;>;>(TT;)V";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertSame(1, methodSignature.getParamTypes().size());
        TypeDesc in = methodSignature.getParamTypes().get(0);
        Assert.assertEquals("T", in.asTypeVariable().className());
        // type variable
        TypeDesc firstTypeVar = methodSignature.getTypeVariables().get(0);
        Assert.assertEquals("T", firstTypeVar.className());
        TypeDesc firstUpper = firstTypeVar.asTypeVariable().getUpperBounds().get(0);
        Assert.assertEquals("java/util/List", firstUpper.className());
        Assert.assertEquals("java/lang/String", firstUpper.asParameterized().getTypeArguments().get(0).className());
    }

    @Test
    public void testComplexTypeVariables() {
        // <T extends Number & Comparable<T>, U> T xxx(U param) throws IOException;
        String signature = "<T:Ljava/lang/Number;:Ljava/lang/Comparable<TT;>;U:Ljava/lang/Object;>(TU;)TT;^Ljava/io/IOException;";
        MethodSignature methodSignature = parseSignature(signature);
        Assert.assertEquals("java/io/IOException", methodSignature.getExceptions().get(0).className());
        List<TypeDesc> typeVariables = methodSignature.getTypeVariables();
        Assert.assertSame(2, typeVariables.size());
        Assert.assertEquals("U", typeVariables.get(1).asTypeVariable().className());
        TypeVariableTypeDesc firstTypeVar = typeVariables.get(0).asTypeVariable();
        Assert.assertEquals("T", firstTypeVar.className());
        Assert.assertEquals("java/lang/Number", firstTypeVar.getUpperBounds().get(0).className());
        TypeDesc secondUpperBound = firstTypeVar.getUpperBounds().get(1);
        Assert.assertEquals("java/lang/Comparable", secondUpperBound.className());
        Assert.assertEquals("T", secondUpperBound.asParameterized().getTypeArguments().get(0).asTypeVariable().className());
    }

    // 解析签名的辅助方法
    private MethodSignature parseSignature(String signature) {
        CustomMethodSignatureVisitor visitor = new CustomMethodSignatureVisitor();
        SignatureReader reader = new SignatureReader(signature);
        reader.accept(visitor);
        return visitor.getMethodSignature();
    }
}
